#pragma once

#include <optional>
#include <sstream>
#include <cstring>
#include <vector>

#include "panda_export.hpp"

namespace Panda {
class PANDA_EXPORT ArgHandler {
 public:
  enum Type {
    None,
    Bool,
    Int,
    String,
  };
  explicit ArgHandler() {}

  template <class T>
  ArgHandler& SetRequired(const char* key, const char* key_info = nullptr) {
    ArgItemValue<T>* item = new ArgItemValue<T>();
    item->key = key;
    item->key_info = key_info;
    item->is_option = false;
    if (std::is_same<T, int>()) {
      item->has_value = Type::Int;
    } else if (std::is_same<T, bool>()) {
      item->has_value = Type::Bool;
    } else if (std::is_same<T, std::string>()) {
      item->has_value = Type::String;
    }
    items.emplace_back(item);
    return *this;
  }

  template <class T>
  ArgHandler& SetOption(const char* key, const char* key_info = nullptr) {
    ArgItemValue<T>* item = new ArgItemValue<T>();
    item->key = key;
    item->key_info = key_info;
    if (std::is_same<T, int>()) {
      item->has_value = Type::Int;
    } else if (std::is_same<T, bool>()) {
      item->has_value = Type::Bool;
    } else if (std::is_same<T, std::string>()) {
      item->has_value = Type::String;
    }
    items.emplace_back(item);
    return *this;
  }

  template <class T>
  T GetValue(const char* key, T def = T()) {
    for (auto& it : items) {
      if (std::strncmp(key, it->key, std::strlen(key)) == 0) {
        if (it->option_set) return *((ArgItemValue<T>*)(it))->value;
        return def;
      }
    }
    return def;
  }

  std::string ToString() const {
    std::stringstream ss;
    for (auto& it : items) {
      ss << it->key;
      if (it->key_info) {
        ss << " \t" << it->key_info;
      }
      if (it->is_option == false) {
        ss << " \t Required";
      }
#ifdef PANDA_WIN32
      ss << " \r\n";
#else
      ss << " \n";
#endif
    }
    return ss.str();
  }

  bool Parse(int argc, const char** argv) {
    for (int i = 1; i < argc; i++) {
      auto it = std::find_if(items.begin(), items.end(), [&](ArgItem* it) {
        if (std::strncmp(it->key, argv[i], std::strlen(it->key)) == 0) {
          return true;
        }
        return false;
      });

      if (it != items.end()) {
        if ((*it)->has_value == Type::Bool) {
          ((ArgItemValue<bool>*)(*it))->value = true;
          (*it)->option_set = true;
          continue;
        } else {
          if ((*it)->has_value == Type::Int) {
            if (i + 1 < argc) {
              ((ArgItemValue<int>*)(*it))->value = std::atoi(argv[i + 1]);
              (*it)->option_set = true;
            } else {
              return false;
            }
          }
          if ((*it)->has_value == Type::String) {
            if (i + 1 < argc) {
              ((ArgItemValue<std::string>*)(*it))->value = argv[i + 1];
              (*it)->option_set = true;
            } else {
              return false;
            }
          }
          i = i + 1;
          continue;
        }
      } else {
        return false;
      }
    }
    for (auto& it : items) {
      if (it->is_option == false && it->option_set == false) {
        return false;
      }
    }
    return true;
  }

 private:
  struct ArgItem {
    const char* key;
    const char* key_info;
    bool is_option = true;
    bool option_set = false;
    Type has_value = Type::None;
  };
  template <class T>
  struct ArgItemValue : public ArgItem {
    std::optional<T> value = std::nullopt;
  };

  std::vector<ArgItem*> items;
};
}  // namespace Panda